/**
* diqube: Distributed Query Base.
*
* Copyright (C) 2015 Bastian Gloeckle
*
* This file is part of diqube.
*
* diqube is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diqube.im.logout;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import org.diqube.consensus.ConsensusClient;
import org.diqube.consensus.ConsensusClient.ClosableProvider;
import org.diqube.consensus.ConsensusClient.ConsensusClusterUnavailableException;
import org.diqube.consensus.ConsensusStateMachineClientInterruptedException;
import org.diqube.context.AutoInstatiate;
import org.diqube.im.logout.LogoutStateMachine.CleanLogoutTicket;
import org.diqube.im.logout.LogoutStateMachineImplementation.LogoutStateMachineListener;
import org.diqube.thrift.base.thrift.Ticket;
import org.diqube.thrift.base.thrift.TicketClaim;
import org.diqube.ticket.TicketValidityService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Cleans Tickets from {@link LogoutStateMachine} that are invalid now anyway (i.e. because the
* {@link TicketClaim#getValidUntil()} has passed, the tickets do nat have to be tracked manually anymore).
*
* @author Bastian Gloeckle
*/
@AutoInstatiate
public class LogoutCleaner implements LogoutStateMachineListener {
private static final Logger logger = LoggerFactory.getLogger(LogoutCleaner.class);
/** max time between polls for old logged out tickets that can be cleaned. */
private static final long MAX_POLL_SEC = 120; // 4 min
@Inject
private ConsensusClient consensusClient;
private LogoutCleanupThread thread;
/** Map from {@link TicketClaim#getValidUntil()} to the tickets that have been invalidated. */
private NavigableMap<Long, Set<Ticket>> timeouts = new ConcurrentSkipListMap<>();
@PostConstruct
public void intialize() {
thread = new LogoutCleanupThread();
thread.start();
}
@PreDestroy
public void cleanup() {
thread.interrupt();
}
@Override
public void ticketBecameInvalid(Ticket t) {
timeouts.compute(t.getClaim().getValidUntil(), (key, oldValue) -> {
Set<Ticket> res = new ConcurrentSkipListSet<>();
res.add(t);
if (oldValue != null)
res.addAll(oldValue);
return res;
});
}
private class LogoutCleanupThread extends Thread {
LogoutCleanupThread() {
super("logout-cleanup-thread");
}
@Override
public void run() {
long randomWaitDiff = ThreadLocalRandom.current().nextLong((MAX_POLL_SEC / 2) * 1_000L);
while (true) {
try {
Thread.sleep((MAX_POLL_SEC / 2) * 1_000L + randomWaitDiff); // random so not everybody executes simultaneously
} catch (InterruptedException e) {
// exit quietly
return;
}
Map<Long, Set<Ticket>> timeoutedTickets =
timeouts.headMap(System.currentTimeMillis() - TicketValidityService.CLEANUP_NO_REMOVE_MOST_RECENT_MS);
if (!timeoutedTickets.isEmpty()) {
logger.info("Found {} tickets that have been logged out but are invalid now anyway. "
+ "Removing them from the consensus cluster.", timeoutedTickets.size());
try (ClosableProvider<LogoutStateMachine> p =
consensusClient.getStateMachineClient(LogoutStateMachine.class)) {
Set<Ticket> allTickets =
timeoutedTickets.values().stream().flatMap(s -> s.stream()).collect(Collectors.toSet());
for (Ticket t : allTickets)
p.getClient().cleanLogoutTicket(CleanLogoutTicket.local(t));
timeoutedTickets.clear();
} catch (ConsensusClusterUnavailableException e) {
logger.warn("Could not contact consensus cluster to remove old logged out tickets.", e);
} catch (ConsensusStateMachineClientInterruptedException e) {
// exit quietly
return;
} catch (RuntimeException e) {
logger.warn("Could not contact consensus cluster to remove old logged out tickets", e);
}
}
}
}
}
}